﻿/*
Copyright (C) 2009  Tomasz Chrzanowski

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Install;
using System.Diagnostics;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;

namespace Com.TomCdc.ChangeDataCapture
{
    /// <summary>
    /// The service class performing processing of the captured changed data in monitored system.
    /// </summary>
    public class CdcProcessorService:ServiceBase
    {
        /// <summary>
        /// Create a logger for use in this class.
        /// </summary>
        private static readonly log4net.ILog log =
            log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// The sevice name.
        /// </summary>
        private const string SERVICE_NAME = CommonHelper.CDC_AGENT_SERVICE_NAME;

        /// <summary>
        /// The default fequency of checking stage database for new data to process,
        /// if not specified in config file.
        /// </summary>
        private readonly TimeSpan DEF_STAGEDB_POLLING_INTERVAL = TimeSpan.FromSeconds(15);

        /// <summary>
        /// The default fequency of checking stage database for new data to process,
        /// in case of recent consecutive failures.
        /// </summary>
        private readonly TimeSpan DEF_STAGEDB_POLLING_INTERVAL_FAILS = TimeSpan.FromMinutes(15);

        /// <summary>
        /// The time after which started service will perform first checking 
        /// of stage database for new data to process.
        /// </summary>
        private readonly TimeSpan DEF_STAGEDB_POLLING_DUE = TimeSpan.FromSeconds(30);

        /// <summary>
        /// The number of consecutive processing failures before polling frequency change to 
        /// <see cref="DEF_STAGEDB_POLLING_INTERVAL_FAILS"/> value.
        /// </summary>
        private const int DEF_FAILURE_LIMIT_BEFORE_DELAY = 10;

        /// <summary>
        /// The instance of main processing class.
        /// </summary>
        private CdcProcessor _cdcWorker;

        /// <summary>
        /// The timer fireing processing peridicly.
        /// </summary>
        private Timer _cdcWorkerTimer;

        /// <summary>
        /// The current fequency of checking stage database for new data to process.
        /// </summary>
        private TimeSpan _currentStageDbPollingPeriod;

        /// <summary>
        /// The frequency of checking stage database for new data to process definied
        /// in application configuration file.
        /// </summary>
        private TimeSpan _configStageDbPollingInterval;

        /// <summary>
        /// The frequency of checking stage database for new data to process in case of
        /// last try filure.
        /// </summary>
        private TimeSpan _stageDbFailedPollingInterval;

        /// <summary>
        /// The counter of consecutive processing failures.
        /// </summary>
        private int _consecutiveFailuresCounter;

        /// <summary>
        /// Object to lock processing section.
        /// </summary>
        private object _lockObject = new object();

        /// <summary>
        /// The status of stop or pause request sent from Service Control Manager.
        /// </summary>
        private bool _stopServiceRequested;

        /// <summary>
        /// Initializes a new instance of the <see cref="CdcProcessorService"/> class.
        /// </summary>
        public CdcProcessorService()
        {
            // initializes with default value
            _currentStageDbPollingPeriod = DEF_STAGEDB_POLLING_INTERVAL;

            // specifies which service command can be performed
            CanPauseAndContinue = true;
            CanStop = true;

            ServiceName = SERVICE_NAME;
        }

        /// <summary>
        /// The entry point of the process.
        /// </summary>
        static void Main(string[] args)
        {
            if (System.Environment.UserInteractive)
            {
                string parameter = string.Concat(args);
                try
                {
                    switch (parameter)
                    {
                        case "--install":
                            ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
                            Console.WriteLine("Service installed successfully.");
                            break;
                        case "--uninstall":
                            ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location });
                            Console.WriteLine("Service uninstalled successfully.");
                            break;
                        default:
                            Console.WriteLine("Usage: CdcProcessorService.exe [--install | --uninstall]");
                            break;
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Problem occured while processing the request." + Environment.NewLine +
                        ex.Message);
                }
            }
            else
            {
                ServiceBase.Run(new CdcProcessorService());
            }
        }

        /// <summary>
        /// Starts the service.
        /// </summary>
        protected override void OnStart(string[] args)
        {
            log.Info("-------- Starting TomCdc Agent Service. ----------");

            // creates objects to access application config file entries
            ConnectionStringSettingsCollection connStrSettings = ConfigurationManager.ConnectionStrings;
            NameValueCollection appConf = ConfigurationManager.AppSettings;

            string noConfValueErrMsg = 
                "Missing or incorrect {keyName} value in the application configuration file. Default used.";

            // gets connection string from the application config file
            try
            {
                _cdcWorker = new CdcProcessor(
                    connStrSettings["CdcStageConnString"].ConnectionString,
                    connStrSettings["CdcStoreConnString"].ConnectionString,
                    connStrSettings["CdcTrackConnString"].ConnectionString
                    );
            }
            catch (Exception ex)
            {
                log.Error("Missing or incorrect connection string values in the configuration file.", ex);
                throw new MissingInformationException(noConfValueErrMsg.Replace("{keyName}",
                    "stage or store connection string"), ex);
            }

            // checks if changeTransPerSqlTran definied in config
            try
            {
                _cdcWorker.ChangeTransPerSqlTran = int.Parse(appConf["changeTransPerSqlTran"]);
            }
            catch(Exception ex)
            {
                log.Error(noConfValueErrMsg.Replace("{keyName}", "changeTransPerSqlTran"), ex);
                EventLog.WriteEntry(noConfValueErrMsg.Replace("{keyName}", "changeTransPerSqlTran") +
                    Environment.NewLine + ex.Message, EventLogEntryType.Warning);
            }

            // initialize chacking period in case of last try failure with default value
            _stageDbFailedPollingInterval = DEF_STAGEDB_POLLING_INTERVAL_FAILS;

            // checks if the polling interval after consecutive failures definied in config file
            try
            {
                // gets value form app config file (unit seconds) and convert to miliseconds
                _stageDbFailedPollingInterval = 
                    TimeSpan.FromSeconds(int.Parse(appConf["stageDbFailedPollingIntervalSec"]));
            }
            catch (Exception ex)
            {
                log.Error(noConfValueErrMsg.Replace("{keyName}", "stageDbFailedPollingIntervalSec"), ex);
                EventLog.WriteEntry(noConfValueErrMsg.Replace("{keyName}","stageDbFailedPollingIntervalSec") +
                    Environment.NewLine + ex.Message, EventLogEntryType.Warning);
            }

            // corrects interval value if needed
            if (_stageDbFailedPollingInterval.TotalSeconds <= 0)
            {
                _stageDbFailedPollingInterval = DEF_STAGEDB_POLLING_INTERVAL_FAILS;
                log.WarnFormat("The config entry stageDbFailedPollingIntervalSec has to be grater than 0. " +
                    "It has been reset to default value of {0} second(s).", DEF_STAGEDB_POLLING_INTERVAL_FAILS.TotalSeconds);
            }

            // initialize chacking period with default value
            _configStageDbPollingInterval = DEF_STAGEDB_POLLING_INTERVAL;

            // checks if the polling interval definied in config file
            try
            {
                // gets value form app config file (unit seconds)
                _configStageDbPollingInterval =
                    TimeSpan.FromSeconds(int.Parse(appConf["stageDbPollingIntervalSec"]));
            }
            catch (Exception ex)
            {
                log.Error(noConfValueErrMsg.Replace("{keyName}", "stageDbPollingIntervalSec"), ex);
                EventLog.WriteEntry(noConfValueErrMsg.Replace("{keyName}", "stageDbPollingIntervalSec") +
                    Environment.NewLine + ex.Message, EventLogEntryType.Warning);
            }

            // corrects interval value if needed
            if (_configStageDbPollingInterval.TotalSeconds <= 0)
            {
                _configStageDbPollingInterval = DEF_STAGEDB_POLLING_INTERVAL;
                log.WarnFormat("The config entry stageDbPollingIntervalSec has to be grater than 0. " +
                    "It has been reset to default value of {0} second(s).",DEF_STAGEDB_POLLING_INTERVAL.TotalSeconds);
            }

            // assign current checking period
            _currentStageDbPollingPeriod = _configStageDbPollingInterval;

            // reset fails counter
            _consecutiveFailuresCounter = 0;

            // sets if requested stop or pause to false
            _stopServiceRequested = false;

            // initializes timer for periodic checkings of stage database
            // service's timer has due time set to DEF_STAGEDB_POLLING_DUE which give the time
            // to database (if used local) to wake up after machine reboot.
            _cdcWorkerTimer = new Timer(new TimerCallback(ExecuteProcessing),
                null, 
                (int)DEF_STAGEDB_POLLING_DUE.TotalMilliseconds,
                (int)_currentStageDbPollingPeriod.TotalMilliseconds);
            log.Info("-------- Starting procedure completed successfully. ----------");
        }

        /// <summary>
        /// Stops the service.
        /// </summary>
        protected override void OnStop()
        {
            log.Info("Service stop procedure started.");
            // sets request to stop processing
            _stopServiceRequested = true;

            // request additional time (90 sec) to finish processing
            RequestAdditionalTime(90000);

            // stop only when processing not active
            lock (_lockObject)
            {
                WaitHandle wh  = new AutoResetEvent(true);

                // disposes the worker timer
                _cdcWorkerTimer.Dispose(wh);
                wh.WaitOne();

                // makes object ready for garbage collector
                _cdcWorker = null;

                // enforce garbage collector to free up cdcWorker memory
                GC.Collect();
            }
            log.Info("Service STOPPED.");
        }

        /// <summary>
        /// Continue the service after pause.
        /// </summary>
        protected override void OnContinue()
        {
            // start the worker timer
            _cdcWorkerTimer.Change(
                (int)DEF_STAGEDB_POLLING_DUE.TotalMilliseconds, 
                (int)_configStageDbPollingInterval.TotalMilliseconds);
            log.Info("Service started after pause.");
        }

        /// <summary>
        /// Pauses running service.
        /// </summary>
        protected override void OnPause()
        {
            // sets request to stop processing
            _stopServiceRequested = true;

            // request additional time (90 sec) to finish processing
            RequestAdditionalTime(90000);

            // pause only when processing not active
            lock (_lockObject)
            {
                // stops main worker's timer
                _cdcWorkerTimer.Change(Timeout.Infinite, Timeout.Infinite);
            }
            log.Info("Service paused.");
        }

        /// <summary>
        /// The main worker method of the service <code>Timer</code>.
        /// </summary>
        /// <param name="stateInfo"></param>
        void ExecuteProcessing(object stateInfo)
        {
            // guarantees that the processing will not be broken by pause or stop service command
            lock (_lockObject)
            {
                try
                {
                    // stop timer before processing (prevent from overlapping)
                    _cdcWorkerTimer.Change(Timeout.Infinite, Timeout.Infinite);

                    // process waiting change transactions until all processed or
                    // the request to stop processing has arrived 
                    while (_cdcWorker.Process() > 0 && !_stopServiceRequested) ;

                    // reset fails counter
                    _consecutiveFailuresCounter = 0;

                    // reinitialize checking period to oryginal value, in case of recent execution failurs
                    // current checking period could have been extended to _stageDbFailedPollingInterval value
                    _currentStageDbPollingPeriod = _configStageDbPollingInterval;
                }
                catch (Exception ex)
                {
                    // increase fails counter
                    _consecutiveFailuresCounter++;
                    log.Error("Error occured while processing transactions.", ex);

                    // check if failures number exceeds definied value, if yes increse polling interval
                    if (_consecutiveFailuresCounter >= DEF_FAILURE_LIMIT_BEFORE_DELAY)
                    {
                        _currentStageDbPollingPeriod = _stageDbFailedPollingInterval;
                    }
                }
                finally
                {
                    // start timer after processing
                    _cdcWorkerTimer.Change(
                        (int)_currentStageDbPollingPeriod.TotalMilliseconds,
                        (int)_currentStageDbPollingPeriod.TotalMilliseconds);
                }
            }
        }

    }
}
